import os, re, sys, json, urllib.parse
import requests
from bs4 import BeautifulSoup

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
}


def sanitize(name: str) -> str:
    return re.sub(r'[\\/:*?"<>|]+', '_', name.strip())


def fetch_bing_image_urls_async(query: str, limit: int = 6):
    urls = []
    params = {
        'q': query,
        'first': 0,
        'count': max(20, limit * 4),
        'relp': max(20, limit * 4),
        'scenario': 'ImageBasicHover',
        'datsrc': 'I',
        'layout': 'ColumnBased',
        'mmasync': 1
    }
    for host in ['https://www.bing.com', 'https://cn.bing.com']:
        try:
            r = requests.get(host + '/images/async', params=params, headers=HEADERS, timeout=12)
            if r.status_code != 200:
                continue
            soup = BeautifulSoup(r.text, 'lxml')
            for a in soup.select('a.iusc'):
                m = a.get('m')
                if not m:
                    continue
                try:
                    meta = json.loads(m)
                    u = meta.get('murl')
                    if u and u.startswith('http') and u not in urls:
                        urls.append(u)
                        if len(urls) >= limit:
                            break
                except Exception:
                    continue
            if len(urls) >= limit:
                break
        except Exception:
            continue
    return urls


def fetch_baidu_image_urls(query: str, limit: int = 6):
    urls = []
    params = {
        'tn': 'resultjson_com',
        'ipn': 'r',
        'ct': 201326592,
        'fp': 'result',
        'queryWord': query,
        'word': query,
        'pn': 0,
        'rn': max(30, limit * 4),
        'gsm': '1e'
    }
    headers = dict(HEADERS)
    headers['Referer'] = 'https://image.baidu.com/'
    try:
        r = requests.get('https://image.baidu.com/search/acjson', params=params, headers=headers, timeout=12)
        r.raise_for_status()
        js = r.json()
        for item in js.get('data', []):
            u = item.get('middleURL') or item.get('hoverURL') or item.get('thumbURL')
            if u and u.startswith('http') and u not in urls:
                urls.append(u)
                if len(urls) >= limit:
                    break
    except Exception:
        return urls
    return urls


def fetch_commons_image_urls(query: str, limit: int = 6):
    urls = []
    api = 'https://commons.wikimedia.org/w/api.php'
    params = {
        'action': 'query',
        'generator': 'search',
        'gsrsearch': query,
        'gsrlimit': max(20, limit * 4),
        'gsrnamespace': 6,  # File namespace
        'prop': 'imageinfo',
        'iiprop': 'url',
        'format': 'json'
    }
    try:
        r = requests.get(api, params=params, headers=HEADERS, timeout=12)
        r.raise_for_status()
        js = r.json()
        pages = js.get('query', {}).get('pages', {})
        for _, p in pages.items():
            infos = p.get('imageinfo') or []
            for info in infos:
                u = info.get('url')
                if u and u.startswith('http') and u not in urls:
                    urls.append(u)
                    if len(urls) >= limit:
                        break
            if len(urls) >= limit:
                break
    except Exception:
        return urls
    return urls


def fetch_sogou_image_urls(query: str, limit: int = 6):
    urls = []
    params = {
        'mode': 20,
        'start': 0,
        'xml_len': max(48, limit * 8),
        'query': query
    }
    headers = dict(HEADERS)
    headers['Referer'] = 'https://pic.sogou.com/'
    try:
        r = requests.get('https://pic.sogou.com/napi/pc/searchList', params=params, headers=headers, timeout=12)
        r.raise_for_status()
        js = r.json()
        data = js.get('data') or js
        arr = data.get('list') or data.get('items') or []
        for item in arr:
            u = item.get('picUrl') or item.get('thumbUrl') or item.get('oriPic')
            if u and u.startswith('http') and u not in urls:
                urls.append(u)
                if len(urls) >= limit:
                    break
    except Exception:
        return urls
    return urls


def fetch_image_urls(query: str, limit: int = 6):
    urls = fetch_bing_image_urls_async(query, limit)
    if len(urls) < limit:
        remaining = limit - len(urls)
        urls.extend(fetch_baidu_image_urls(query, remaining))
    if len(urls) < limit:
        remaining = limit - len(urls)
        urls.extend(fetch_sogou_image_urls(query, remaining))
    if len(urls) < limit:
        remaining = limit - len(urls)
        urls.extend(fetch_commons_image_urls(query, remaining))
    return urls


def download_image(url: str, out_dir: str, idx: int):
    os.makedirs(out_dir, exist_ok=True)
    path = urllib.parse.urlparse(url).path
    ext = os.path.splitext(path)[1]
    if not ext or len(ext) > 5:
        ext = '.jpg'
    fp = os.path.join(out_dir, f'image_{idx}{ext}')
    try:
        with requests.get(url, headers=HEADERS, timeout=15, stream=True) as r:
            if r.status_code == 200:
                with open(fp, 'wb') as f:
                    for chunk in r.iter_content(8192):
                        if chunk:
                            f.write(chunk)
                return fp
    except Exception:
        return None
    return None


def crawl_images(scene: str, base_dir: str, limit_each: int = 5):
    views = ['正面', '背面', '左侧', '右侧', '全景']
    saved = []
    for v in views:
        q = f'{scene} {v}'
        out_dir = os.path.join(base_dir, 'images', sanitize(v))
        try:
            urls = fetch_image_urls(q, limit_each)
        except Exception:
            urls = []
        i = 1
        for u in urls:
            p = download_image(u, out_dir, i)
            if p:
                saved.append(p)
                i += 1
    return saved


def wiki_open_search(scene: str):
    api = 'https://zh.wikipedia.org/w/api.php'
    params = {'action': 'opensearch', 'search': scene, 'limit': 1, 'namespace': 0, 'format': 'json'}
    try:
        r = requests.get(api, params=params, headers=HEADERS, timeout=10)
        r.raise_for_status()
        js = r.json()
        if js and len(js) >= 2 and js[1]:
            return js[1][0]
    except Exception:
        return None
    return None


def wiki_parse_infobox(title: str):
    api = 'https://zh.wikipedia.org/w/api.php'
    params = {'action': 'parse', 'page': title, 'prop': 'text', 'format': 'json'}
    try:
        r = requests.get(api, params=params, headers=HEADERS, timeout=12)
        r.raise_for_status()
        js = r.json()
        html = js.get('parse', {}).get('text', {}).get('*', '')
        soup = BeautifulSoup(html, 'lxml')
        t = soup.find('table', {'class': 'infobox'})
        info = {}
        if t:
            for tr in t.select('tr'):
                th = tr.find('th')
                td = tr.find('td')
                if th and td:
                    k = th.get_text(strip=True)
                    v = td.get_text(' ', strip=True)
                    info[k] = v
        return info
    except Exception:
        return {}


def baike_fetch(scene: str):
    url = 'https://baike.baidu.com/item/' + urllib.parse.quote(scene)
    info = {}
    try:
        r = requests.get(url, headers=HEADERS, timeout=12)
        r.raise_for_status()
        soup = BeautifulSoup(r.text, 'lxml')
        # v1: 经典 basicInfo-block
        for dl in soup.select('dl.basicInfo-block'):
            dts = dl.select('dt.basicInfo-item')
            dds = dl.select('dd.basicInfo-item')
            for dt, dd in zip(dts, dds):
                k = dt.get_text(strip=True)
                v = dd.get_text(' ', strip=True)
                info[k] = v
        # v2: 新版 basic-info 容器
        if not info:
            div = soup.select_one('div.basic-info')
            if div:
                dts = div.select('dt')
                dds = div.select('dd')
                for dt, dd in zip(dts, dds):
                    k = dt.get_text(strip=True)
                    v = dd.get_text(' ', strip=True)
                    info[k] = v
    except Exception:
        return {}
    return info


def extract_metrics(info: dict):
    area = None
    height = None
    for k, v in info.items():
        if area is None and any(x in k for x in ['面积', '占地', '景区面积', '总面积', '建筑面积']):
            area = v
        if height is None and any(x in k for x in ['高度', '建築高度', '建筑高度', '楼高', '塔高', '通高']):
            height = v
        # 进一步从值中抓取数字+单位（即使键名不完全匹配）
        if height is None and ('高' in k or '高' in v):
            m = re.search(r'(\d+(?:\.\d+)?)\s*(米|m|M)', v)
            if m:
                height = f"{_fmt_num(m.group(1))} 米"
        if area is None and ('面积' in k or '占地' in k or '面积' in v or '占地' in v):
            m = re.search(r'(\d+(?:\.\d+)?)\s*(平方米|平米|m²|㎡|公顷|亩|平方公里|km²)', v)
            if m:
                unit = normalize_unit_str(m.group(2))
                area = f"{_fmt_num(m.group(1))} {unit}"
    return area, height

UNIT_MAP = {
    'Q11573': '米',          # metre
    'Q125929': '厘米',        # centimetre
    'Q174728': '毫米',        # millimetre
    'Q828224': '公里',        # kilometre
    'Q712226': '平方米',      # square metre
    'Q35852': '公顷',         # hectare
    'Q25343': '平方公里',     # square kilometre (可能不完全准确)
}


def _fmt_num(v: str) -> str:
    try:
        f = float(v)
        s = f"{f:.2f}"
        return s.rstrip('0').rstrip('.')
    except Exception:
        return v


def format_amount(amount: str, unit_url: str) -> str:
    unit_qid = None
    if unit_url and unit_url.startswith('http'):
        unit_qid = unit_url.rsplit('/', 1)[-1]
    label = UNIT_MAP.get(unit_qid)
    val = amount.lstrip('+') if isinstance(amount, str) else str(amount)
    num = _fmt_num(val)
    return f"{num} {label}" if label else num


def wiki_get_qid(title: str):
    api = 'https://zh.wikipedia.org/w/api.php'
    params = {'action': 'query', 'titles': title, 'prop': 'pageprops', 'format': 'json'}
    try:
        r = requests.get(api, params=params, headers=HEADERS, timeout=10)
        r.raise_for_status()
        js = r.json()
        pages = js.get('query', {}).get('pages', {})
        for _, page in pages.items():
            qid = page.get('pageprops', {}).get('wikibase_item')
            if qid:
                return qid
    except Exception:
        return None
    return None


def wikidata_fetch_claims(qid: str):
    api = 'https://www.wikidata.org/w/api.php'
    params = {'action': 'wbgetentities', 'ids': qid, 'props': 'claims', 'format': 'json'}
    try:
        r = requests.get(api, params=params, headers=HEADERS, timeout=12)
        r.raise_for_status()
        js = r.json()
        return js.get('entities', {}).get(qid, {}).get('claims', {})
    except Exception:
        return {}


def wikidata_extract_metrics(claims: dict):
    area = None
    height = None
    def extract_first_amount(pid: str):
        arr = claims.get(pid, [])
        for cl in arr:
            snak = cl.get('mainsnak', {})
            val = snak.get('datavalue', {}).get('value', {})
            amount = val.get('amount')
            unit = val.get('unit')
            if amount:
                return format_amount(amount, unit)
        return None
    height = extract_first_amount('P2048')  # height
    area = extract_first_amount('P2046')    # area
    return area, height


def wiki_extract_plaintext(title: str) -> str:
    api = 'https://zh.wikipedia.org/w/api.php'
    params = {'action': 'query', 'prop': 'extracts', 'exintro': 1, 'explaintext': 1, 'titles': title, 'format': 'json'}
    try:
        r = requests.get(api, params=params, headers=HEADERS, timeout=10)
        r.raise_for_status()
        js = r.json()
        pages = js.get('query', {}).get('pages', {})
        for _, page in pages.items():
            ext = page.get('extract')
            if ext:
                return ext
    except Exception:
        return ''
    return ''


def normalize_unit_str(unit: str) -> str:
    u = unit.lower()
    if u in ['m', '米']:
        return '米'
    if u in ['m²', '㎡', '平方米', '平米']:
        return '平方米'
    if u in ['km²', '平方公里']:
        return '平方公里'
    if u in ['公顷']:
        return '公顷'
    if u in ['亩']:
        return '亩'
    return unit


def extract_metrics_from_text(text: str):
    area = None
    height = None
    try:
        m = re.search(r'(?:建筑高度|楼高|塔高|通高|高度)[^\d]{0,10}(\d+(?:\.\d+)?)\s*(米|m|M)', text)
        if m:
            height = f"{_fmt_num(m.group(1))} 米"
        if not height:
            m = re.search(r'(?:高达|高约|高近|高)\s*(\d+(?:\.\d+)?)\s*米', text)
            if m:
                height = f"{_fmt_num(m.group(1))} 米"
        m = re.search(r'(?:景区面积|总面积|占地|建筑面积|面积)[^\d]{0,14}(\d+(?:\.\d+)?)\s*(平方米|平米|m²|㎡|公顷|亩|平方公里|km²)', text)
        if m:
            unit = normalize_unit_str(m.group(2))
            area = f"{_fmt_num(m.group(1))} {unit}"
        if not area:
            m = re.search(r'(?:占地|占地面积)\s*(\d+(?:\.\d+)?)\s*(亩|公顷)', text)
            if m:
                area = f"{_fmt_num(m.group(1))} {m.group(2)}"
    except Exception:
        pass
    return area, height


def bing_text_search_extract(query: str):
    params = {'q': query}
    try:
        r = requests.get('https://www.bing.com/search', params=params, headers=HEADERS, timeout=12)
        r.raise_for_status()
        html = r.text
        soup = BeautifulSoup(html, 'lxml')
        blocks = soup.select('li.b_algo, ol#b_results > li')
        texts = []
        for b in blocks[:10]:
            texts.append(b.get_text(' ', strip=True))
        combined = '\n'.join(texts)
        # 如果块为空，退化为整页文本
        if not combined.strip():
            combined = soup.get_text(' ', strip=True)
        return extract_metrics_from_text(combined)
    except Exception:
        return (None, None)


def baike_extract_summary(scene: str) -> str:
    url = 'https://baike.baidu.com/item/' + urllib.parse.quote(scene)
    try:
        r = requests.get(url, headers=HEADERS, timeout=12)
        r.raise_for_status()
        soup = BeautifulSoup(r.text, 'lxml')
        div = soup.select_one('div.lemma-summary')
        if div:
            return div.get_text(' ', strip=True)
    except Exception:
        return ''
    return ''


def maybe_prompt_missing(metrics: dict):
    try:
        if not sys.stdin.isatty():
            return metrics
    except Exception:
        return metrics
    print('提示：未能自动获取完整的面积/高度，可在下方手动补全，直接回车跳过。')
    if not metrics.get('height'):
        h = input('请输入主要建筑高度（示例：51.4 米）：').strip()
        if h:
            metrics['height'] = h
            metrics['source'] = metrics.get('source') or 'manual'
    if not metrics.get('area'):
        a = input('请输入景区面积（示例：150 亩 或 50000 平方米）：').strip()
        if a:
            metrics['area'] = a
            metrics['source'] = metrics.get('source') or 'manual'
    return metrics


def crawl_info(scene: str):
    metrics = {'scene': scene, 'source': None, 'area': None, 'height': None}
    title = wiki_open_search(scene) or scene
    # 1) Wikidata 结构化属性
    try:
        qid = wiki_get_qid(title)
        if qid:
            claims = wikidata_fetch_claims(qid)
            a, h = wikidata_extract_metrics(claims)
            if a or h:
                metrics['source'] = 'wikidata'
                metrics['area'] = a or metrics['area']
                metrics['height'] = h or metrics['height']
    except Exception:
        pass
    # 2) Wikipedia 信息框
    if not metrics['area'] or not metrics['height']:
        inf = wiki_parse_infobox(title)
        a, h = extract_metrics(inf)
        if a or h:
            metrics['source'] = metrics['source'] or 'zh.wikipedia'
            metrics['area'] = metrics['area'] or a
            metrics['height'] = metrics['height'] or h
    # 3) Wikipedia 正文正则
    if not metrics['area'] or not metrics['height']:
        text = wiki_extract_plaintext(title)
        a, h = extract_metrics_from_text(text)
        if a or h:
            metrics['source'] = metrics['source'] or 'zh.wikipedia'
            metrics['area'] = metrics['area'] or a
            metrics['height'] = metrics['height'] or h
    # 4) Baidu Baike 信息框
    if not metrics['area'] or not metrics['height']:
        inf = baike_fetch(scene)
        a, h = extract_metrics(inf)
        if a or h:
            metrics['source'] = metrics['source'] or 'baike.baidu'
            metrics['area'] = metrics['area'] or a
            metrics['height'] = metrics['height'] or h
    # 5) Baike 正文正则
    if not metrics['area'] or not metrics['height']:
        txt = baike_extract_summary(scene)
        a, h = extract_metrics_from_text(txt)
        if a or h:
            metrics['source'] = metrics['source'] or 'baike.baidu'
            metrics['area'] = metrics['area'] or a
            metrics['height'] = metrics['height'] or h
    # 6) Bing 搜索结果页正则
    if not metrics['area'] or not metrics['height']:
        a1, h1 = bing_text_search_extract(f"{scene} 面积")
        a2, h2 = bing_text_search_extract(f"{scene} 高度 或 楼高")
        a = a1 or a2
        h = h1 or h2
        if a or h:
            metrics['source'] = metrics['source'] or 'bing.search'
            metrics['area'] = metrics['area'] or a
            metrics['height'] = metrics['height'] or h
    return metrics


def save_results(scene: str, base: str, metrics: dict):
    os.makedirs(base, exist_ok=True)
    with open(os.path.join(base, 'info.json'), 'w', encoding='utf-8') as f:
        json.dump(metrics, f, ensure_ascii=False, indent=2)
    lines = [
        f'场景：{scene}',
        f'数据源：{metrics.get("source")}',
        f'景区面积：{metrics.get("area") or "未知"}',
        f'主要建筑高度：{metrics.get("height") or "未知"}',
    ]
    with open(os.path.join(base, 'info.txt'), 'w', encoding='utf-8') as f:
        f.write('\n'.join(lines))


def run(scene: str):
    base = os.path.join('output', sanitize(scene))
    print(f'开始爬取：{scene}')
    imgs = crawl_images(scene, base, limit_each=2)
    metrics = crawl_info(scene)
    metrics = maybe_prompt_missing(metrics)
    save_results(scene, base, metrics)
    print(f'完成：共保存{len(imgs)}张图片，信息文件位于 {base}')


if __name__ == '__main__':
    if len(sys.argv) >= 2:
        s = sys.argv[1]
    else:
        s = input('请输入要爬取的场景名称：').strip()
    if not s:
        print('场景名称为空，退出。')
        sys.exit(1)
    run(s)
